package config

import (
	"encoding/json"
	"fmt"
	"regexp"
	"strconv"
	"strings"
	"time"

	"github.com/alecthomas/units"
)

const (
	dayHours  = 24
	weekHours = 7 * 24
	yearHours = 365 * 24
)

var (
	// Regexp for day specifications in durations
	durationDayRe = regexp.MustCompile(`(\d+(?:\.\d+)?)d`)
	// Regexp for week specifications in durations
	durationWeekRe = regexp.MustCompile(`(\d+(?:\.\d+)?)w`)
	// Regexp for year specifications in durations
	durationYearRe = regexp.MustCompile(`(\d+(?:\.\d+)?)y`)
)

// Duration is a time.Duration
type Duration time.Duration

// Set implements pflag/flag.Value
func (d *Duration) Set(s string) error {
	var err error
	*d, err = ParseDuration(s)
	return err
}

// Type implements pflag.Value
func (d *Duration) Type() string {
	return "duration"
}

func (d Duration) String() string {
	var (
		ns = int64(d)
		r  = ""
	)
	if ns == 0 {
		return "0s"
	}

	f := func(unit string, mult int64, exact bool) {
		if exact && ns%mult != 0 {
			return
		}
		if v := ns / mult; v > 0 {
			r += fmt.Sprintf("%d%s", v, unit)
			ns -= v * mult
		}
	}

	// Only format years and weeks if the remainder is zero, as it is often
	// easier to read 90d than 12w6d.
	f("y", int64(time.Hour)*24*365, true)
	f("w", int64(time.Hour)*24*7, true)

	f("d", int64(time.Hour)*24, false)
	f("h", int64(time.Hour), false)
	f("m", int64(time.Minute), false)
	f("s", int64(time.Second), false)
	f("ms", int64(time.Millisecond), false)
	f("us", int64(time.Microsecond), false)
	f("ns", int64(time.Nanosecond), false)

	return r
}

// MarshalText implements the encoding.TextMarshaler interface.
func (d *Duration) MarshalText() ([]byte, error) {
	return []byte(d.String()), nil
}

// UnmarshalTOML parses the duration from the TOML config file
func (d *Duration) UnmarshalText(b []byte) (err error) {
	// convert to string
	durStr := string(b)

	*d, err = ParseDuration(durStr)
	return err
}

// MarshalJSON implements the json.Marshaler interface.
func (d Duration) MarshalJSON() ([]byte, error) {
	return json.Marshal(d.String())
}

// UnmarshalJSON implements the json.Unmarshaler interface.
func (d *Duration) UnmarshalJSON(bytes []byte) error {
	var s string
	if err := json.Unmarshal(bytes, &s); err != nil {
		return err
	}
	dur, err := ParseDuration(s)
	if err != nil {
		return err
	}
	*d = dur
	return nil
}

// MarshalYAML implements the yaml.Marshaler interface.
func (d Duration) MarshalYAML() (any, error) {
	return d.String(), nil
}

// UnmarshalYAML implements the yaml.Unmarshaler interface.
func (d *Duration) UnmarshalYAML(unmarshal func(any) error) error {
	var s string
	if err := unmarshal(&s); err != nil {
		return err
	}
	dur, err := ParseDuration(s)
	if err != nil {
		return err
	}
	*d = dur
	return nil
}

// Size is an int64
type Size int64

func (s *Size) UnmarshalText(b []byte) error {
	if len(b) == 0 {
		return nil
	}

	str := string(b)
	val, err := strconv.ParseInt(str, 10, 64)
	if err == nil {
		*s = Size(val)
		return nil
	}
	val, err = units.ParseStrictBytes(str)
	if err != nil {
		return err
	}
	*s = Size(val)
	return nil
}

func toHours(regexpPrt *regexp.Regexp, convHours float64, durStr string) string {
	for _, m := range regexpPrt.FindAllStringSubmatch(durStr, -1) {
		num, err := strconv.ParseFloat(m[1], 64)
		if err != nil {
			continue
		}
		hours := strconv.FormatFloat(num*convHours, 'f', -1, 64) + "h"
		durStr = strings.Replace(durStr, m[0], hours, 1)
	}
	return durStr
}

func ParseDuration(durStr string) (Duration, error) {
	sI, err := strconv.ParseInt(durStr, 10, 64)

	if err == nil {
		dur := time.Second * time.Duration(sI)
		return Duration(dur), nil
	}
	// Second try parsing as float seconds
	sF, err := strconv.ParseFloat(durStr, 64)
	if err == nil {
		dur := float64(time.Second) * sF
		return Duration(dur), nil
	}

	// Finally, try value is a TOML string (e.g. "3s", 3s) or literal (e.g. '3s')
	if durStr == "" {
		return Duration(0), nil
	}

	// Handle "day", "week", "year" intervals and replace them with the "hours" equivalent
	durStr = toHours(durationDayRe, dayHours, durStr)
	durStr = toHours(durationWeekRe, weekHours, durStr)
	durStr = toHours(durationYearRe, yearHours, durStr)

	dur, err := time.ParseDuration(durStr)
	if err != nil {
		return Duration(0), err
	}

	return Duration(dur), nil
}
